// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_SOCKET_SENDER_H_
#define SCRW_SOCKET_SENDER_H_

#include <cstring>
#include <stdexcept>
#include <libv/cast.h>
#include <libv/check.h>
#include <libv/error_msg.h>

#ifdef _WIN32
# include <winsock.h>
# include <windows.h>

# pragma warning(push)
# pragma warning(disable:4127)  // FD_SET conditional expression is constant
#else
# include <sys/types.h>
# include <sys/socket.h>
# include <sys/time.h>
# include <netdb.h>
# include <fcntl.h>
# include <unistd.h>
#endif  // _WIN32


// Если не определен макрос — определяем, а после include возвращаем как было.
#ifndef LOKI_CLASS_LEVEL_THREADING
# define LOKI_CLASS_LEVEL_THREADING
# define UNDEF_LOKI_CLASS_LEVEL_THREADING
#endif
#include <loki/Threads.h>
#include <loki/Singleton.h>
#ifdef UNDEF_LOKI_CLASS_LEVEL_THREADING
# undef LOKI_CLASS_LEVEL_THREADING
# undef UNDEF_LOKI_CLASS_LEVEL_THREADING
#endif


namespace scrw
{

/** RAII-объект, инициализирующий сокеты в конструкторе и очищающий в
  * деструкторе. Так как создавать объект нужно один раз, рекомендуется
  * использование класса-одиночки (синглтона)
  * TheSocketsInitializationAndCleanUp, в таком случае первый вызов
  * TheSocketsInitializationAndCleanUp::Instance() проведет
  * инициализацию, а последующие будут проигнорированы. */
class SocketsInitializationAndCleanUp
{
  public:
    SocketsInitializationAndCleanUp()
    {
#ifdef _WIN32
        WSADATA wsa_data;
        VERIFY(::WSAStartup(MAKEWORD(2,2), &wsa_data) == 0);
#endif
    }

    ~SocketsInitializationAndCleanUp()
    {
#ifdef _WIN32
        ::WSACleanup();
#endif
    }
};


typedef Loki::SingletonHolder<
    SocketsInitializationAndCleanUp,
    Loki::CreateUsingNew,
    Loki::DefaultLifetime,
    Loki::ClassLevelLockable> TheSocketsInitializationAndCleanUp;





/** Базовое исключение, генерируемое классом SocketsSaver при возникновении
  * любой ошибки. */
class SocketSenderException : public ::std::exception
{
  public:
    explicit SocketSenderException(const ::std::string& msg) : msg_(msg) {}
    virtual ~SocketSenderException() throw() {}
    virtual const char* what() const throw() { return msg_.c_str(); }

  private:
    ::std::string msg_;
};


/** Исключение, генерируемое классом SocketsSaver при потере соединения. */
class ConnectionLostException : public SocketSenderException
{
  public:
    explicit ConnectionLostException()
        : SocketSenderException("Connection lost") {}

    virtual ~ConnectionLostException() throw() {}

    virtual const char* what() const throw()
    {
        return SocketSenderException::what();
    }
};


/** Исключение, генерируемое классом SocketsSaver при достижении таймаута .*/
class TimeoutException : public SocketSenderException
{
public:
    explicit TimeoutException()
        : SocketSenderException("Operation timeout") {}

    virtual ~TimeoutException() throw() {}

    virtual const char* what() const throw()
    {
        return SocketSenderException::what();
    }
};





/** Класс, позволяющий передавать сообщения через сокеты.
  * Основан на счетчиках ссылок, таким образом, копия объекта будет ссылаться
  * на тот же системный ресурс. Класс не потокобезопасен.
  * Класс реализован с использованием неблокирующих сокетов: на все операции в
  * конструкторе задается таймаут, при достижении которого будет сгенерировано
  * исключение TimeoutException. Кроме указанного выше, генерируется еще 2 типа
  * исключений: ConnectionLostException в случае, если соединение с сервером
  * потеряно, в таком случае можно попробовать восстановить его вызовом метода
  * Reconnect() и SocketSenderException, при любой другой ошибке.
  * Поддерживается tcp протокол, ip4 и ip6. */
class SocketSender
{
  public:

    /** Конструктор по умолчанию, используемый в основном для инициализации
      * стандартных контейнеров. Созданный таким образом объект считается
      * неинициализированным.
      * @sa IsInited(). */
    SocketSender()
        : serv_list_(NULL),
          serv_(NULL),
          socket_(0),
          timeout_(0),
          ref_count_(NULL)  {}


    /** Конструктор.
      * @pre server.empty() == false
      * @pre port.empty() == false.
      * @param[in] server имя сервера или его ip-адрес
      * @param[in] port порт или имя протокола сервера
      * @param[in] timeout таймаут в секундах для всех выполняемых операций.
      * @exception TimeoutException, SocketSenderException. */
    SocketSender(const ::std::string& server,
                 const ::std::string& port,
                 const long timeout)
        : serv_list_(NULL),
          serv_(NULL),
          socket_(0),
          timeout_(timeout),
          ref_count_(new int(1))
    {
        PRECOND(server.empty() == false);
        PRECOND(port.empty() == false);

        // Инициализация сокетов, которая будет произведена 1 раз.
        TheSocketsInitializationAndCleanUp::Instance();

        ::std::memset(&hints_, 0, sizeof(hints_));
        hints_.ai_family = AF_UNSPEC;     // ip4 or ip6
        hints_.ai_socktype = SOCK_STREAM; // tcp
        hints_.ai_flags = AI_PASSIVE;     // заполнить мой ip за меня

        GetAddressInfo(server, port);

        GetSocket();
        ChangeSocketNonBlockingFlag(true);
        Connect();
    }


    /** Конструктор копирования. Так как класс основан на счетчиках ссылок,
      * то скопированный объект будет ссылаться на тот же ресурс системы. */
    SocketSender(const SocketSender& rhv)
        : hints_(rhv.hints_),
          serv_list_(rhv.serv_list_),
          serv_(rhv.serv_),
          socket_(rhv.socket_),
          timeout_(rhv.timeout_),
          ref_count_(rhv.ref_count_)
    {
        if ( IsInited() )  // скопировали инициализированный объект
            IncRefCount();
    }


    /** Оператор присваивания. Так как класс основан на счетчиках ссылок,
      * то присвоенный объект будет ссылаться на тот же ресурс системы.
      * Метод не потокобезопасен. Не генерирует исключений. */
    SocketSender& operator=(const SocketSender& rhv)
    {
        if ( this != &rhv )
        {
            // Являемся инициализированным объектом.
            if ( IsInited() && DecRefCount() == 0 )
                Destruct();

            this->hints_ = rhv.hints_;
            this->serv_list_ = rhv.serv_list_;
            this->serv_ = rhv.serv_;
            this->socket_ = rhv.socket_;
            this->timeout_ = rhv.timeout_;
            this->ref_count_ = rhv.ref_count_;

            if ( IsInited() )
                IncRefCount();
        }

        return *this;
    }


    /** Деструктор. Так как класс основан на счетчиках ссылок,
      * то ресурс системы будет освобожден при удалении последнего ссылающегося
      * на него объекта. */
    ~SocketSender()
    {
        if ( IsInited() && DecRefCount() == 0 )
            Destruct();
    }


    /** Проверка того, что объект инициализирован (то есть был создан не
      * конструктором по умолчанию). Выполенение метода является предусловием
      * для всех методов класса.
      * Метод не потокобезопасен. Не генерирует исключений. */
    bool IsInited() const
    {
        return ref_count_ != NULL;
    }


    /** Переподключение к серверу. Метод может быть использован после генерации
      * исключения ConnectionLostException. Метод не потокобезопасен.
      * @pre IsInited().
      * @exception TimeoutException, SocketSenderException,
      *            libv::PrecondException. */
    void Reconnect()
    {
        PRECOND(IsInited());
        ChangeSocketNonBlockingFlag(false);
        Disconnect();

        GetSocket();
        ChangeSocketNonBlockingFlag(true);
        Connect();
    }


    /** Отправка данных удаленному серверу. Метод не потокобезопасен.
      * @pre IsInited()
      * @pre data != NULL
      * @pre data_size > 0.
      * @param[in] data передаваемые данные
      * @param[in] data_size размер данных.
      * @exception ConnectionLostException, TimeoutException,
      *            SocketSenderException, libv::PrecondException. */
    void Send(const char* data, const size_t data_size)
    {
        PRECOND(IsInited());
        PRECOND(data != NULL);
        PRECOND(data_size > 0);

        for ( size_t sent_size = 0 ; sent_size < data_size ; )
        {
            fd_set w_fds;
            FD_ZERO(&w_fds);
            FD_SET(socket_, &w_fds);
            fd_set e_fds = w_fds;

            struct timeval to;
            to.tv_sec = timeout_;
            to.tv_usec = 0;

            // Windows игнорирует 1 аргумент, так что преобразование не мешает.
            // В Linux Socket имеет тип int.
            // POSIX требует int первым аргументом у select.
            const int r = ::select((int)socket_ + 1, NULL, &w_fds, &e_fds, &to);

            if ( r == 0 )
                throw TimeoutException();
            if ( r == -1 )
                throw SocketSenderException(libv::StrError());
            if ( FD_ISSET(socket_, &e_fds) )
                throw SocketSenderException("Socket send error");

#ifdef _WIN32
            const int flags = 0;
#else
            const int flags = MSG_NOSIGNAL;
#endif

            const int s = ::send(socket_,
                                 data + sent_size,
                                 libv::IntegerCast<int>(data_size - sent_size),
                                 flags);

            if ( s == -1 )
            {
                bool connection_closed = false;
#ifdef _WIN32
                const int err = ::WSAGetLastError();
                connection_closed = err == WSAECONNABORTED || // пришел FIN
                                    err == WSAECONNRESET ||   // разрыв
                                    err == WSAESHUTDOWN;      // не должно быть
#else
                connection_closed = errno == EPIPE;
#endif

                if ( connection_closed )
                    throw ConnectionLostException();
                throw SocketSenderException(libv::StrError());
            }

            sent_size += libv::IntegerCast<size_t>(s);
        }
    }



  private:
#ifdef _WIN32
    typedef SOCKET Socket;
#else
    typedef int Socket;
#endif


    /** Уменьшение счетчика ссылок на 1. Возвращается новое значение. */
    int DecRefCount() const
    {
        PRECOND(IsInited());
        return --(*ref_count_);
    }


    /** Увеличение счетчика ссылок на 1. Возвращается новое значение. */
    int IncRefCount() const
    {
        PRECOND(IsInited());
        return ++(*ref_count_);
    }


    void Destruct()
    {
        PRECOND(IsInited());
        ::freeaddrinfo(serv_list_);
        serv_ = NULL;
    }


    void GetAddressInfo(const ::std::string& address, const ::std::string& port)
    {
        PRECOND(IsInited());
        const int r = ::getaddrinfo(address.c_str(), port.c_str(),
                                    &hints_, &serv_list_);
        if ( r != 0 )
            throw SocketSenderException(GetAddressInfoErrorMessage(r));
    }


    void GetSocket()
    {
        PRECOND(IsInited());
        for ( addrinfo* p = serv_list_ ; p != NULL ; p = p->ai_next )
        {
            const Socket s = ::socket(p->ai_family, p->ai_socktype, 
                                      p->ai_protocol);

            if ( s != -1 )
            {
                socket_ = s;
                serv_ = p;
                return;
            }
        }

        throw SocketSenderException("GetSocket failed");
    }


#ifdef _WIN32
    void ChangeSocketNonBlockingFlag(const bool non_blocking)
    {
        PRECOND(IsInited());
        u_long value = static_cast<int>(non_blocking);
        if ( ::ioctlsocket(socket_, FIONBIO, &value ) == -1 )
        {
            throw SocketSenderException(
                libv::OsErrorCode2String(::WSAGetLastError()));
        }
    }
#else
    void ChangeSocketNonBlockingFlag(const bool non_blocking)
    {
        PRECOND(IsInited());
        int flags = ::fcntl(socket_, F_GETFL, 0);

        if ( flags == -1 )
            throw SocketSenderException(libv::StrError());

        if ( non_blocking )
            flags |= O_NONBLOCK;
        else
            flags &= ~O_NONBLOCK;

        if ( ::fcntl(socket_, F_SETFL, flags) == -1 )
            throw SocketSenderException(libv::StrError());
    }
#endif


    void Connect()
    {
        PRECOND(IsInited());
#ifdef _WIN32  // Microsoft сами в своих типах не могут разобраться!
        const int rc = ::connect(socket_, serv_->ai_addr,
                                 (int)serv_->ai_addrlen);
        if ( rc == 0 ) return;
        if ( rc == -1 && ::WSAGetLastError() != WSAEWOULDBLOCK )
        {
            throw SocketSenderException(
                libv::OsErrorCode2String(::WSAGetLastError()));
        }
#else
        const int rc = ::connect(socket_, serv_->ai_addr, serv_->ai_addrlen);
        if ( rc == 0 ) return;
        if ( rc == -1 && errno != EAGAIN )
            throw SocketSenderException(libv::StrError());
#endif

        fd_set r_fds;
        FD_ZERO(&r_fds);
        FD_SET(socket_, &r_fds);
        fd_set w_fds = r_fds;
        fd_set e_fds = r_fds;

        timeval to;
        to.tv_sec = timeout_;
        to.tv_usec = 0;

        const int rs = ::select((int)socket_ + 1, &r_fds, &w_fds, &e_fds, &to);

        if ( rs == 0 )
            throw TimeoutException();
        if ( rs == -1 )
            throw SocketSenderException(libv::StrError());
        if ( FD_ISSET(socket_, &e_fds) )
            throw SocketSenderException(libv::StrError());

        assert(rs == 1);
        assert(FD_ISSET(socket_, &r_fds) || FD_ISSET(socket_, &w_fds));

        int err = 0;
#ifdef _WIN32
        int len = sizeof(err);
#else
        socklen_t len = sizeof(err);
#endif
        if ( ::getsockopt(socket_, SOL_SOCKET, SO_ERROR, (char*)&err, &len) )
            throw SocketSenderException(libv::StrError());
    }


    /** Проверка того, что серврер "вежливо" отключился. */
    bool ServerSentFinPacket() const
    {
        PRECOND(IsInited());
        timeval to;
        to.tv_sec = 0;
        to.tv_usec = 0;

        fd_set r_fds;
        FD_ZERO(&r_fds);
        FD_SET(socket_, &r_fds);
        fd_set e_fds = r_fds;

        const int rs = ::select((int)socket_ + 1, &r_fds, NULL, &e_fds, &to);

        if ( rs == 0 )
            return false;
        if ( rs == -1 )
            throw SocketSenderException(libv::StrError());
        if ( FD_ISSET(socket_, &e_fds) )
            throw SocketSenderException("Socket check FIN packet error");

        assert(rs == 1);

        char buf;
        const int rr = ::recv(socket_, &buf, sizeof(buf), 0);

        if ( rr == -1 )
            throw SocketSenderException(libv::StrError());
        else if ( rr == 0 )
            return true;
        else
            throw SocketSenderException("Server disconnection check error"
                                        " got unexpected data");
    }


    int Disconnect()
    {
        PRECOND(IsInited());
        if ( socket_ == 0 ) return true;

        const Socket tmp = socket_;
        socket_ = 0;

#ifdef _WIN32
        return ::closesocket(tmp) == 0;
#else
        return ::close(tmp) == 0;
#endif
    }



#ifdef _WIN32
    ::std::string GetAddressInfoErrorMessage(const int err) const
    {
        const DWORD max_msg_len = 1024 + 1;
        char buf[max_msg_len];
        // Используется FormatMessageA, а не strerror, так как
        // Windows использует расширенный набор кодов ошибок.
        const DWORD msg_len = ::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
                                               FORMAT_MESSAGE_IGNORE_INSERTS |
                                               FORMAT_MESSAGE_MAX_WIDTH_MASK,
                                               NULL,
                                               err,
                                               MAKELANGID(LANG_NEUTRAL,
                                                          SUBLANG_NEUTRAL),
                                               (LPSTR)buf,
                                               max_msg_len,
                                               NULL);

        return ::std::string(buf, msg_len);
    }
#else
    ::std::string GetAddressInfoErrorMessage(const int err) const
    {
        const char* s = ::gai_strerror(err);
        if ( s == NULL ) return ::std::string();
        return ::std::string(s);
    }
#endif


    addrinfo hints_;
    addrinfo* serv_list_;
    addrinfo* serv_;

    Socket socket_;

    long timeout_;

    mutable int* ref_count_;
};

}  // scrw

#ifdef _WIN32
# pragma warning(pop)
#endif

#endif  // SCRW_SOCKET_SENDER_H_
